package hydrap2p.search;

import hydrap2p.library.HashData;
import hydrap2p.logging.Level;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Search {
	private int searchid;
	private Map<Integer, SearchResult> results;
	private Map<String, SearcherIface> providers;
	private Map<String, SearcherIface> totalproviders;
	private List<String> activeproviders;
	private final SearchQuery query;
	private static final String TAG="Search - Execution";
	private boolean stopping;
	private Listeners listeners;
	
	private static Pattern searchparser = Pattern.compile("(?:([^\\s\"]\\S*)|(?:\"((?:i|[^i])*?)(?:(?:\"\\s)|(?:\"$)|$)))");		// http://forums.java.net/jive/thread.jspa?messageID=23980
	private List<String> searchAll;
	private List<String> searchAny;
	private List<String> searchNot;
	
	public Search(int searchid, Map<String, SearcherIface> providers, Listeners listener, SearchQuery query)
	{
		this.searchid=searchid;
		results=new TreeMap<Integer, SearchResult>();
		totalproviders=new TreeMap<String, SearcherIface>(providers);
		activeproviders=new LinkedList<String>();
		this.providers=providers;
		this.listeners=listener;
		this.query=query;
		
		if (query.metadata!=null && query.metadata.containsKey("any") && query.metadata.get("any")!=null)
			searchAny=parseAdvancedSearch(query.metadata.get("any"));
		if (query.metadata!=null && query.metadata.containsKey("all") && query.metadata.get("all")!=null)
			searchAll=parseAdvancedSearch(query.metadata.get("all"));
		if (query.metadata!=null && query.metadata.containsKey("not") && query.metadata.get("not")!=null)
			searchNot=parseAdvancedSearch(query.metadata.get("not"));
	}
	
	private static List<String> parseAdvancedSearch(String tokens)
	{
		List<String> ret=new LinkedList<String>();
		Matcher matcher = searchparser.matcher(tokens);
		while (matcher.find()) {
			String token=matcher.group(matcher.group(1)==null ? 2 : 1);
			if (token!=null)
				ret.add(token);
		}
		return ret;
	}
	
	public Search(Search clone)
	{
		searchid=clone.searchid;
		results=new TreeMap<Integer, SearchResult>(clone.results);
		providers=new TreeMap<String, SearcherIface>(clone.providers);
		totalproviders=new TreeMap<String, SearcherIface>(clone.totalproviders);
		activeproviders=new LinkedList<String>(clone.activeproviders);
		query=clone.query.deepCopy();
		stopping=clone.stopping;
		listeners=clone.listeners;
	}
	
	public void start()
	{
		List<String> failed=new LinkedList<String>();
		synchronized(totalproviders)
		{
			activeproviders.clear();
			for (Entry<String, SearcherIface> provider : totalproviders.entrySet())
			{
				try {
					provider.getValue().startSearch(searchid, query);
					activeproviders.add(provider.getKey());
				} catch (Exception e) {
					failed.add(provider.getKey());
					SearchLogging.log(Level.MISHAP, TAG, "Failed to signal a search provider to start a search, skipping");
				}
			}
		}
		synchronized(providers)
		{
			for (String provider : failed)
			{
				providers.remove(provider);
			}
		}

		if (getSearchActiveProviders().size()==0)
			listeners.searchFinished(searchid);
		else
			for (String provider:activeproviders)
				listeners.searchProviderStarted(searchid, provider);
	}
	
	public void stop()
	{
		stopping=true;
		List<String> failed=new LinkedList<String>();
		synchronized(totalproviders)
		{
			for (Entry<String, SearcherIface> provider : totalproviders.entrySet())
			{
				if (activeproviders.contains(provider.getKey()))
				{
					try {
						provider.getValue().stopSearch(searchid);
						activeproviders.remove(provider.getKey());
					} catch (Exception e) {
						failed.add(provider.getKey());
					}
				}
			}
		}
		listeners.searchFinished(searchid);
		stopping=false;
	}
	
	public int getSearchId()
	{
		return searchid;
	}
	
	private boolean doesSearchResultMatch(SearchResult result)
	{
		boolean matches=false;
		
		// presplit the names
		// Check to see if anything in "any" phrases
		if (searchAny==null || searchAny.size()==0)
			matches=true;		// by default, assume that it matches whatever Any they wanted
		for (String name : result.names)
		{
			if (matches || searchAny==null) break;
			for (String keyword : searchAny)
			{
				if (matches) break;
				if (name.toLowerCase().contains(keyword.toLowerCase()))
					matches=true;
			}
		}
		
		// Check to see that it contains all of "all" phrases
		for (String name : result.names)
		{
			if (!matches || searchAll==null) break;
			for (String keyword : searchAll)
			{
				if (!matches) break;
				if (!name.toLowerCase().contains(keyword.toLowerCase()))
					matches=false;
			}
		}
		
		// Check that it doesn't contain anything from "not" phrases
		for (String name : result.names)
		{
			if (!matches || searchNot==null) break;
			for (String keyword : searchNot)
			{
				if (!matches) break;
				if (name.toLowerCase().contains(keyword.toLowerCase()))
					matches=false;
			}
		}
		return matches;
	}
	
	/**
	 * Returns the ID of the result, or -1 if it wasn't added
	 * @param result
	 * @return
	 */
	public int addSearchResult(SearchResult result)
	{
		boolean matches=doesSearchResultMatch(result);
		if (!matches) return -1;
		
		int resultid=-1;
		// validate the result
		if (result.hashes==null)
			result.hashes=new LinkedList<HashData>();
		if (result.metadata==null)
			result.metadata=new TreeMap<String, String>();
		if (result.extra==null)
			result.extra=new TreeMap<String, String>();
		synchronized(results)
		{
			boolean found=false;
			for (SearchResult needle : results.values())
			{
				if (similarSearchResult(needle, result))
				{
					mergeList(needle.sources, result.sources, true);
					mergeList(needle.hashes, result.hashes, true);
					mergeList(needle.names, result.names, false);
					mergeMap(needle.metadata, result.metadata, true);
					mergeMap(needle.extra, result.extra, true);
					resultid=needle.resultid;
					found=true;
				}
			}
			if (!found)
			{
				resultid=results.size();
				result.resultid=resultid;
				results.put(results.size(), result);
			}
		}
		listeners.addSearchResult(searchid, result);
		SearchLogging.log(Level.INFO, "Search - Adding Result", "Added search result id "+resultid+" to search "+query.name);
		return resultid;
	}
	
	public SearchResult getSearchResult(int resultid)
	{
		return results.get(resultid);
	}
	
	public static boolean similarSearchResult(SearchResult left, SearchResult right)
	{
		boolean found=false;
		boolean similar=true;
		if (left.hashes!=null && right.hashes!=null)
		{
			for (HashData hash : left.hashes)
			{
				if (hash.range.len==left.size)	// is a hash across the entire file
				{
					for (HashData righthash : right.hashes)
					{
						if (hash.range.equals(righthash.range) &&
							hash.algorithm.equals(righthash.algorithm))
						{
							found=true;
							if (hash.hashdata.equals(righthash.hashdata))
								similar|=true;
							else
								similar=false;
						}
					}
				}
			}
		}
		return found && similar;
	}
	
	/**
	 * Copies items from the right list to the left list
	 * @param left
	 * @param right
	 * @param overwrite If true, overwrite sources on the left with new, updated ones on the right
	 */
	public static <T> void mergeList(List<T> left, List<T> right, boolean overwrite)
	{
		synchronized(left)
		{
			for (T object: right)
			{
				boolean contains=left.contains(object);
				if (contains && overwrite)
				{
					left.remove(object);
					contains=false;
				}
				if (!contains )
					left.add(object);
			}
		}
	}
	
	/**
	 * Copies items from the right list to the left list
	 * @param left
	 * @param right
	 * @param overwrite If true, overwrite sources on the left with new, updated ones on the right
	 */
	public static <T,U> void mergeMap(Map<T,U> left, Map<T,U> right, boolean overwrite)
	{
		synchronized(left)
		{
			for (Entry<T,U> object: right.entrySet())
			{
				boolean contains=left.containsKey(object.getKey());
				if (contains && overwrite)
				{
					left.remove(object.getKey());
					contains=false;
				}
				if (!contains )
					left.put(object.getKey(), object.getValue());
			}
		}
	}
	
	public List<SearchResult> getSearchResults()
	{
		return new LinkedList<SearchResult>(results.values());
	}
	
	public void searchFinished(String provider)
	{
		if (!stopping)
		{
			synchronized(activeproviders)
			{
				activeproviders.remove(provider);
			}
		}
		listeners.searchProviderFinished(searchid, provider);
		if (getSearchActiveProviders().size()==0)
			listeners.searchFinished(searchid);
		SearchLogging.log(Level.INFO, "Search - Search Provider Finished", "Search Provider "+provider+" finished searching "+query.name);
	}
	public List<String> getSearchActiveProviders()
	{
		return activeproviders;
	}
	public List<String> getSearchTotalProviders()
	{
		return new LinkedList<String>(totalproviders.keySet());
	}
	public SearchQuery getSearchQuery()
	{
		return query;
	}
}
